package org.hawk.gwt.ppc.parser.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;

import org.hawk.gwt.ppc.parser.NoSuchTokenException;
import org.hawk.gwt.ppc.parser.PreprocessorIOException;
import org.hawk.gwt.ppc.parser.PreprocessorParseException;
import org.hawk.gwt.ppc.parser.Token;
import org.hawk.gwt.ppc.utils.PreprocessorException;

/**
 * Splits characters from some reader on tokens.
 * Java token is represented as either identifier or operator or single character.
 * 
 * @author alex.bereznevatiy@gmail.com
 */
final class Tokenizer {
	
	/**
	 * Default token type if others are not applied.
	 */
	private static final int TOKEN_DEFAULT = 0;
	
	/**
	 * Token type for java word literal 
	 */
	private static final int TOKEN_JAVA_WORLD = 1;
	
	/**
	 * Token type for digit literal
	 */
	private static final int TOKEN_DIGIT_LITERAL = 2;
	
	/**
	 * Token type for character literal
	 */
	private static final int TOKEN_CHAR_LITERAL = 4;
	
	/**
	 * Token type for string literal
	 */
	private static final int TOKEN_STRING_LITERAL = 8;
	
	/**
	 * Token type for comment
	 */
	private static final int TOKEN_COMMENT = 16;
	
	private BufferedReader reader;
	private Token current;
	private Token next;
	private int nextChar;
	private int currentLine = 1;
	private int currentOffset;
	private boolean eof;
	
	/**
	 * Creates tokenizer based on reader.
	 * 
	 * @param reader
	 */
	Tokenizer(Reader reader) throws PreprocessorException {
		if (reader instanceof BufferedReader) {
			this.reader = (BufferedReader) reader;
		} else {
			this.reader = new BufferedReader(reader);
		}
		next = readNext();
	}

	/**
	 * @return current token or throws {@link NoSuchTokenException} in case there are no tokens yet.
	 */
	Token current() {
		if (current == null) {
			throw new NoSuchTokenException();
		}
		return current;
	}

	/**
	 * @return <code>true</code> if there are some more tokens to read.
	 */
	boolean hasNext() {
		return next != null;
	}

	/**
	 * @return next token read from input source
	 * @throws PreprocessorException in case of problems
	 */
	Token next() throws PreprocessorException {
		if (next == null) {
			throw new NoSuchTokenException();
		}
		current = next;
		next = readNext();
		return current;
	}

	/**
	 * Mark this tokenizer as reached end of file.
	 * After this any parsing should be stopped.
	 */
	void eof() {
		this.eof  = true;
	}
	
	/**
	 * @return <code>true</code> if this tokenizer is marked as reached end of file.
	 */
	boolean isEof() {
		return eof;
	}

	private Token readNext() throws PreprocessorIOException, PreprocessorParseException {
		try {
			if (nextChar == 0) {
				nextChar = reader.read();
				currentOffset++;
				skipWhiteSpaces();
			}
			if (nextChar < 0) {
				return null;
			}
			int line = currentLine, offset = currentOffset, type = TOKEN_DEFAULT;
			
			StringBuilder result = new StringBuilder();
			
			result.append((char)nextChar);
			
			if (Character.isJavaIdentifierStart(nextChar)) {
				readName(result);
				type = TOKEN_JAVA_WORLD;
			} else if (nextChar == '\'') {
				readCharOrStringLiteral(result, '\'');
				type = TOKEN_CHAR_LITERAL;
			} else if (nextChar == '"') {
				readCharOrStringLiteral(result, '"');
				type = TOKEN_STRING_LITERAL;
			} else if (Character.isDigit(nextChar)) {
				readDigit(result);
				type = TOKEN_DIGIT_LITERAL;
			} else if (nextChar == '/') {
				nextChar = reader.read();
				currentOffset ++;
				result.append((char)nextChar);
				if (nextChar == '*') {
					readMultilineComment(result);
					type = TOKEN_COMMENT;
				} else if (nextChar == '/') {
					readSingleLineComment(result);
					type = TOKEN_COMMENT;
				} else {
					readComplexOperator(result);
				}
			} else {
				readComplexOperator(result);
			}
			
			skipWhiteSpaces();
			
			char[] rez = new char[result.length()];
			result.getChars(0, rez.length, rez, 0);
			return new ArrayToken(rez, type, line, offset);
		} catch (IOException ex) {
			throw new PreprocessorIOException(ex);
		}
	}

	private void readComplexOperator(StringBuilder result) throws IOException, PreprocessorParseException {
		if (result.length() == 2) {
			// Handle "tail" from comments parsing block
			if (result.charAt(1) == '=') {
				nextChar = reader.read();
				currentOffset ++;
			} else {
				result.deleteCharAt(1);
			}
			return;
		}
		int c = reader.read();
		currentOffset ++;
		if (c != '=' && c != nextChar) {
			nextChar = c;
			return;
		}

		result.append((char)c);
		if (c == '=') {
			c = nextChar;
			nextChar = reader.read();
			if (c != '>' && c != '<' && c != '*' && c != '/' && c != '+' &&
				c != '-' && c != '=' && c != '%' && c != '|' && c != '&' &&
				c != '^' && c != '!') {
				throw new PreprocessorParseException("Unknown operator '" + result + "'", currentLine, currentOffset);
			}
			return;
		}
		
		if (c == '+' || c == '-' || c == '&' || c == '|') {
			nextChar = reader.read();
			return;
		}
		
		if (c != '>' && c != '<') {
			result.deleteCharAt(result.length() - 1);
			nextChar = c;
			return;
		}
		
		c = reader.read();
		currentOffset ++;
		
		if (c != '=' && c != '>') {
			nextChar = c;
			return;
		}
		
		result.append((char)c);
		
		if (c == '=') {
			nextChar = reader.read();
			return;
		}
		
		c = reader.read();
		currentOffset ++;
		
		if (c == '=') {
			result.append((char)c);
			c = reader.read();
			currentOffset ++;
		}
		nextChar = c;
	}

	private void readDigit(StringBuilder result) throws IOException {
		int c;
		while ((c = reader.read()) >= 0 &&
				(Character.isJavaIdentifierPart(c) || c == '.')) {
			result.append((char)c);
		}
		nextChar = c;
	}

	private void readCharOrStringLiteral(StringBuilder result, char quote) throws PreprocessorParseException, IOException {
		int c;
		boolean escaped = false;
		while ((c = reader.read()) >= 0) {
			if (c == '\n' || c == '\r') {
				throw new PreprocessorParseException("Unexpected end of line", currentLine, currentOffset);
			}
			result.append((char)c);
			if (escaped) {
				escaped = false;
			} else if(c == '\\') {
				escaped = true;
			} else if(c == quote) {
				nextChar = reader.read();
				return;
			}
		}
		throw new PreprocessorParseException("Unexpected end of file", currentLine, currentOffset);
	}

	private void readSingleLineComment(StringBuilder result) throws IOException {
		int c;
		while ((c = reader.read()) >= 0 && c != '\n' && c != '\r') {
			result.append((char)c);
		}
		nextChar = c;
	}

	private void readMultilineComment(StringBuilder result) throws PreprocessorParseException, IOException {
		int c, prev = 0;
		boolean flag = false;
		while ((c = reader.read()) >= 0) {
			currentOffset++;
			result.append((char)c);
			if (prev == '\r' && c == '\n') {
				continue;
			}
			if (c == '\n' || c == '\r') {
				currentOffset = 1;
				currentLine++;
			}
			if (c == '*') {
				flag = true;
			} else if (c == '/' && flag) {
				nextChar = reader.read();
				return;
			} else {
				flag = false;
			}
			prev = c;
		}
		throw new PreprocessorParseException("Non-terminated comment", currentLine, currentOffset);
	}

	private void readName(StringBuilder result) throws IOException {
		int c;
		while ((c = reader.read()) >= 0 && Character.isJavaIdentifierPart(c)) {
			currentOffset++;
			result.append((char)c);
		}
		currentOffset++;
		nextChar = c;
	}

	private void skipWhiteSpaces() throws IOException {
		int c = nextChar, prev = 0;
		while (c >= 0 && Character.isWhitespace(c)) {
			if (prev == '\r' && c == '\n') {
				// skip
			} else if (c == '\n' || c == '\r') {
				currentOffset = 1;
				currentLine++;
			} else {
				currentOffset++;
			}
			prev = c;
			c = reader.read();
		}
		nextChar = c;
	}

	/**
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class ArrayToken implements Token {
		
		private char[] array;
		private int line;
		private int offset;
		private int type;

		public ArrayToken(char[] array, int type, int line, int offset) {
			this.array = array;
			this.line = line;
			this.offset = offset;
			this.type = type;
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.CharSequence#length()
		 */
		public int length() {
			return array.length;
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.CharSequence#charAt(int)
		 */
		public char charAt(int index) {
			if (index < 0 || index >= array.length) {
				throw new IndexOutOfBoundsException(String.valueOf(index));
			}
			return array[index];
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.CharSequence#subSequence(int, int)
		 */
		public CharSequence subSequence(int start, int end) {
			return new String(array, start, end - start);
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#replace(java.lang.CharSequence)
		 */
		public void replace(CharSequence replacement) {
			throw new UnsupportedOperationException("Not implemented yet");
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#getLine()
		 */
		public int getLine() {
			return line;
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#getOffset()
		 */
		public int getOffset() {
			return offset;
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			return 31 * line + offset;
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj instanceof ArrayToken) {
				ArrayToken other = (ArrayToken) obj;
				return Arrays.equals(other.array, array);
			}
			return false;
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString() {
			return new String(array);
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#equalsTo(java.lang.CharSequence)
		 */
		public boolean equalsTo(CharSequence keyword) {
			if (array.length != keyword.length()) {
				return false;
			}
			for (int i = 0; i < array.length; i++) {
				if (array[i] != keyword.charAt(i)) {
					return false;
				}
			}
			return true;
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#writeTo(java.lang.StringBuilder)
		 */
		public void writeTo(StringBuilder result) {
			result.append(array);
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#equalsTo(char)
		 */
		public boolean equalsTo(char c) {
			return array.length == 1 && array[0] == c;
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#isWord()
		 */
		public boolean isWord() {
			return (type & TOKEN_JAVA_WORLD) > 0;
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#isDigitLiteral()
		 */
		public boolean isDigitLiteral() {
			return (type & TOKEN_DIGIT_LITERAL) > 0;
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#isStringLiteral()
		 */
		public boolean isStringLiteral() {
			return (type & TOKEN_STRING_LITERAL) > 0;
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#isCharacterLiteral()
		 */
		public boolean isCharacterLiteral() {
			return (type & TOKEN_CHAR_LITERAL) > 0;
		}

		/*
		 * (non-Javadoc)
		 * @see org.hawk.gwt.ppc.parser.Token#isComment()
		 */
		public boolean isComment() {
			return (type & TOKEN_COMMENT) > 0;
		}
	}
}
